fix(platform-objects): translate the zh-CN object labels, options and help that shipped as English copies - #20490
Conversation
… help that shipped as English copies Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
…ter the translations (pnpm i18n:extract) The extractor drops a leaf's copy record once the leaf is no longer a byte copy of its source: 320 rows go, the 42 leaves kept in English stay. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
…sh and pin the reproduced strings A decision ledger for the objects bundle, in the shape the metadata-form ledgers use: 42 leaves declared English by design with a reason each, and the two reproduced strings pinned translated. The nine panel ledgers' zh-CN provenance floors assumed the old copy debt (more than 100 or 300 rows, more than 50 echoing objects leaves); they now ask for a non-empty table and at least one sample, and the Access Token TTL near-twin records that zh-CN is now authored. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
…ranslations Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
…-cn-object-labels
📓 Docs Drift CheckThis PR changes 1 package(s): 29 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1a7ebc6e875a2f266885c0eb9e33746d52d3cd6c && git checkout 1a7ebc6e875a2f266885c0eb9e33746d52d3cd6c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fc0db22bcfdbdb778945317fc4de6dc46aab966d 927957df4e375e0d7c338d7e63e859335b643d3c && git checkout -B drift-repro fc0db22bcfdbdb778945317fc4de6dc46aab966d && git merge --no-ff 927957df4e375e0d7c338d7e63e859335b643d3c
node scripts/docs-audit/affected-docs.mjs --json fc0db22bcfdbdb778945317fc4de6dc46aab966d
|
Contract reviewServed-tier: Inputs, and nothing else: card #20462 body and all four comments (triage 5873680175, claim 5875144601, os-dev-report 5875946281, amended claim 5875978969); PR #20490 body, its 13-file list and the net diff against merge-base ① Derived judgments
Gate verdicts, final read 2026-09-28T18:35:07Z: 34 check-runs, 31 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke — path / opt-in skips), 0 in progress, 0 failed. Lint & Repo Gates (runs ② Semver level
Changeset sentences: the title sentence TRUE (the 320 copies are gone; the 42 that remain are declared, as the third bullet says). The symptom sentence TRUE per the card (not re-measured in a browser). "byte copies of the English source that PR-body sentences: "Fixes #20462" TRUE. The what-changed bullets (320 / 42 / regenerated / pin / ja-JP and es-ES untouched) TRUE, each measured above; "The 42 are set-equal to the declared echoes" TRUE. The coverage-ratchet paragraph TRUE (baseline untouched; the ledger carries no table ⊆ echoes assertion). The count table: every cell TRUE; "Both bundles have 1529 leaves" TRUE; "byte-identical between 3cf6449 and b810ddb, and this walk reads 362 / 216 at 3cf6449 too" TRUE; "the card does not state its method" TRUE. " ③ Boundary flags
Implemented-by: VERDICT: PASS |
…s, and the ledger header says what it asserts All four placeholders were decided as echoes, so the changeset's translated class list drops them. The ledger header now names the two assertion directions instead of claiming the provenance rows equal the echoes. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
…-cn-object-labels
Contract reviewServed-tier: Delta review on top of PASS 5876239898 at ① Derived judgments
Gate verdicts, final read 2026-09-28T18:51:38Z: 40 check-runs on the head — the body edit at 18:47 re-triggered the PR-metadata workflows, so seven names appear twice — 29 success, 5 skipped (the second Auto Label and Check PR Size runs, Build Docs, Console Pin Gate, Packed-tarball smoke), 6 in progress, 0 failed. In progress: Lint & Repo Gates (the ② Semver levelUnchanged: PR-body sentences changed this round: the H4 sentence TRUE (①.4); the ledger-history sentences TRUE (①.5). Patch-round-1 section, sentence by sentence: "The contract review 5876239898 (PASS at 08ebcd0) named one false word and two comment slips. All three are corrected here, as text only; no translated value, assertion or floor changed." TRUE (①.1). The changeset bullet TRUE. The ledger-header bullet TRUE, including "comment only" and "measured, and deliberately not asserted". The "This body" bullet TRUE (both edits are as diffed). "origin/main fc0db22 was merged with a true merge commit" TRUE (①.6). "At head 927957d, ③ Boundary flags
Implemented-by: VERDICT: PASS |
…ptions and help that shipped as English copies (objectstack-ai#20530) Fixes objectstack-ai#20493 Clause-②: no ## What changed A ja-JP or es-ES console read English on Setup surfaces: the Invite user dialog's role list carried **Delegated Admin** beside the translated roles, and every Setup record page read the same copied leaves. The keys were present in `packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts` and `es-ES.objects.generated.ts`, but their values were byte copies of the `en` source that `os i18n extract --fill=default` seeds. This PR applies the zh-CN mechanism of PR objectstack-ai#20490 (squash `9bf5e67af`) to both locales, one PR, one ledger per locale. - **ja-JP: 340 copied leaves translated**, 43 kept as written by design. - **es-ES: 338 copied leaves translated**, 54 kept as written by design. - Leaf values only; the structure of both bundles is untouched. `pnpm i18n:extract` re-read both bundles byte-identical. - **Two ledgers**, one per locale, of the shape `objects-zh-cn-echo-decisions.test.ts` built (`type Verdict = 'translate' | 'echo'`, a reason on every row, a departure on every echo, `undeclaredEchoes` proved on a synthetic row): `objects-ja-jp-echo-decisions.test.ts` and `objects-es-es-echo-decisions.test.ts`. No new mechanism. - **Provenance companions** `ja-JP.source-hashes.generated.ts` and `es-ES.source-hashes.generated.ts` regenerated by `pnpm i18n:extract`, never by hand: −340 and −338 rows, 0 added. The rows that remain are set-equal to each ledger's declared echoes (43 and 54). - **One sibling ledger followed:** `object-lifecycle-panel-echo-decisions.test.ts` pinned `sys_oauth_resource.fields.access_token_ttl.label` as an unauthored fill in ja-JP and es-ES. Both are now authored (アクセストークン TTL, TTL del token de acceso), so the test now asserts all three locales authored with the `TTL` token kept and no provenance row. The other eight sibling ledgers already asked for a non-empty table (`> 0`) since PR objectstack-ai#20490 and needed no edit. - zh-CN, `*.metadata-forms.generated.ts`, the hand-authored `ja-JP.ts` / `es-ES.ts` and objectui are not touched. **The coverage-ratchet half is out of scope, per triage (5877552039, 「新增门禁默认否」).** Teaching `i18n-coverage-baseline` to count a copied source string as untranslated widens an existing gate; if the maintainer wants that ratchet, it is a separate gate card. For the same reason neither ledger asserts that no other leaf of its locale is a copy. ## Pinned strings | leaf | ja-JP | es-ES | pin | |---|---|---|---| | `sys_member.fields.role.options.delegated_admin` | 委任管理者 | Administrador delegado | exact word, equal to the console's | | `sys_invitation.fields.role.options.delegated_admin` | 委任管理者 | Administrador delegado | equal to the `sys_member` options, one word per role | | `sys_team.fields.member_count.label` | メンバー数 | Número de miembros | not the `en` source, no provenance row | The console's words were read at objectui `main` `797a30f`, read-only: `packages/i18n/src/locales/ja.ts`, key `organization.roles.delegatedAdmin` = 委任管理者; `packages/i18n/src/locales/es.ts`, same key = Administrador delegado. The role test also asserts the four role words are distinct, identical on both objects, Japanese-script in ja-JP and none equal to its `en` source in es-ES. ## Copy count, before and after, by key class A string leaf counts as a copy when it is byte-equal to the `en` leaf at the same path, over every leaf of `en.objects.generated.ts` and the locale's bundle. All three bundles have 1529 leaves. "Before" is measured at base `397572ed5`; the card's `b810ddb6f` counts are reproduced exactly (H1 holds). | key class | ja-JP before | ja-JP after | es-ES before | es-ES after | |---|---:|---:|---:|---:| | `label` (object, field, view, action param) | 210 | 29 | 217 | 38 | | `options.*` | 20 | 10 | 22 | 12 | | `pluralLabel` | 12 | 0 | 12 | 0 | | `help` | 121 | 0 | 121 | 0 | | `description` | 12 | 0 | 12 | 0 | | `placeholder` | 4 | 4 | 4 | 4 | | `emptyState.title` / `.message` | 4 | 0 | 4 | 0 | | **total** | **383** | **43** | **392** | **54** | | `label` + `options.*` | 230 | 39 | 239 | 50 | `help` / `description` / `placeholder` / `emptyState` were decided as the same class as `label` (H5), as zh-CN did: the extractor files each under the same `source` kind as its element's label (`packages/cli/src/utils/i18n-extract.ts`). All four placeholders in both locales are typed literals and stay as written. ## Kept as written by design — ja-JP (43) | leaves | value(s) | reason | |---|---|---| | `sys_account._actions.link_social.params.provider.options.{google, github, microsoft, apple, facebook, gitlab, discord}` (7) | Google, GitHub, Microsoft, Apple, Facebook, GitLab, Discord | Sign-in provider brands. The bundle renders no brand name in kana: the authored SSO empty state keeps Okta, Entra and Auth0 verbatim. | | `fields.id.label` on the 23 objects zh-CN lists (`sys_oauth_*` ×7, `sys_sso_provider`, the eight `sys_scim_*`, `sys_email_template`, `sys_metadata`, `sys_metadata_history`, `sys_view_definition`, `sys_metadata_audit`, `sys_secret`, `sys_setting_audit`) (23) | ID | The bundle keeps ID verbatim in every label that carries it (ユーザー ID, 組織 ID, クライアント ID); a bare ID has no noun to render. | | `sys_oauth_application.fields.jwks.label`, `.jwks_uri.label` (2) | JWKS, JWKS URI | Protocol names: `sys_jwks` is authored 署名キー (JWKS), and URIs stay URI (リダイレクト URI). Their `help` leaves are translated. | | `sys_oauth_application._actions.create_oauth_application.params.type.options.web` (1) | Web | The bundle already renders the sibling `sys_oauth_application.fields.type.options.web` as Web; the dialog and the record field read one word. | | `sys_sso_provider._actions.register_saml_provider.params.entryPoint.label` (1) | IdP SSO URL | Protocol initialisms only; the sibling SAML params are authored IdP エンティティ ID and IdP 署名証明書. | | `sys_sso_provider._actions.register_sso_provider.params.{scopes, mapEmail, mapName}.placeholder` (3) | openid email profile, email, name | Typed literals: OIDC scope and claim names; the authored help on the same params keeps them (既定値は「email」). | | `sys_sso_provider._actions.register_saml_provider.params.identifierFormat.placeholder` (1) | the SAML NameID emailAddress URN | Typed literal: an identifier, not prose. | | `sys_email.fields.message_id.label` (1) | Message-ID | RFC 5322 header name; the authored help keeps it (トランスポートが割り当てる RFC-5322 Message-ID). | | `sys_email.fields.cc_addresses.label`, `.bcc_addresses.label` (2) | Cc, Bcc | Header abbreviations Japanese mail clients print in Latin letters, with no kana or kanji form in use. The prose labels on the same record are authored (送信元, 宛先, and 返信先 for Reply-To in this PR). | | `sys_setting_audit.fields.source.options.{ui, api}` (2) | UI, API | Initialisms the bundle keeps (API キー, UI には表示されません; `sys_metadata.fields.source.options.api` is authored API). | Where ja-JP departs from zh-CN: the result dialog's `Client ID` / `Client Secret` were never copies here (authored クライアント ID / クライアントシークレット); `Web` and `Cc` / `Bcc` are ja-JP decisions of their own. ## Kept as written by design — es-ES (54) | leaves | value(s) | reason | |---|---|---| | the 7 sign-in provider options | Google … Discord | Brands; the authored SSO empty state keeps Okta, Entra and Auth0 verbatim. | | `fields.id.label` on the same 23 objects (23) | ID | The bundle keeps ID verbatim in every label that carries it (ID de usuario, ID de organización, ID de cliente). | | `sys_oauth_application.fields.jwks.label`, `.jwks_uri.label` (2) | JWKS, JWKS URI | `sys_jwks` is authored Clave de firma (JWKS); URIs stay URI (URI de redirección). | | `...create_oauth_application.params.type.options.web` (1) | Web | The sibling `fields.type.options.web` is authored Web. | | the 3 OIDC placeholders + the SAML NameID URN placeholder (4) | openid email profile, email, name, URN | Typed literals; the authored help keeps them (Valor predeterminado: «email»). | | `sys_email.fields.message_id.label` (1) | Message-ID | Authored help: Message-ID RFC-5322 asignado por el transporte. | | `sys_email.fields.cc_addresses.label` (1) | Cc | The Spanish copy abbreviation (con copia) is Cc too; the blind-copy sibling is authored Cco. | | `sys_setting_audit.fields.source.options.{ui, api}` (2) | UI, API | Initialisms the bundle keeps (nunca se muestra en la UI; `sys_metadata` source option `api` is authored API). | | `sys_email.fields.error.label`, `sys_job_run.fields.error.label` (2) | Error | Spanish spells it the same (el error); the bundle writes Último error on `sys_job_queue`. | | `sys_notification.fields.actor_id.label`, `sys_metadata_audit.fields.actor.label`, `sys_setting_audit.fields.actor_id.label` (3) | Actor | Spanish spells it the same (el actor); authored help reads Último actor que escribió esta fila, and the messaging bundle authors its actor label Actor. | | `sys_setting.fields.scope.options.global`, `sys_setting_audit.fields.scope.options.global` (2) | Global | Spanish spells it the same, beside the authored sibling options Inquilino and Usuario. | | `sys_email_template.fields.variables_json.label` (1) | Variables (JSON) | Spanish spells it the same; the authored description of the same object says (asunto + contenido + variables). | | `sys_oauth_access_token.fields.token.label`, `sys_oauth_refresh_token.fields.token.label` (2) | Token | The bundle's own word for the concept (Token de sesión, Token de acceso). | | `sys_metadata.fields.checksum.label`, `sys_metadata_history.fields.checksum.label` (2) | Checksum | The bundle's own word (Checksum anterior). | | `sys_organization.fields.slug.label` (1) | Slug | The bundle's own word (Cambiar slug); the console says the same (Ese slug ya está en uso). | Where es-ES departs from zh-CN: `Client ID` / `Client Secret` in the result dialog and `IdP SSO URL` were never copies here (authored); the 14 Spanish-as-written rows above are es-ES's own class. ## Terminology (H4: one term, one rendering) - **ja-JP** takes the bundle's already-authored words: 作成日時 / 更新日時 / 有効期限 / 失効日時 / ローテーション日時, クライアント ID, スコープ, クレーム (claim, as in マッピング: メールクレーム), プロビジョニング (as in IdP プロビジョニング), 投影 and 付与 (as `sys_scim_projection_grant.display_title` authors them), リース and 冪等キー (`sys_job_queue`), トピック / 重大度 / 情報・警告・重大 / 操作者 (the messaging bundle), ポジション (the security bundle), 管理者 for `Managed By` (as `sys_metadata`), 所属組織 for My Memberships (the account nav's label, `ja-JP.ts` `nav_account_memberships`), 配属 for Placement (objectui's word). Decommission is 廃止; tombstone is トゥームストーン, the service-storage ja-JP objects bundle's existing rendering. - **es-ES** likewise: Creado el / Actualizado el / Caduca el / Revocado el / Rotado el, ID de cliente, Ámbitos, reclamo (claim, as in Mapeo: reclamo de correo), aprovisionamiento (as in Aprovisionada por IdP), concesión (as in Tipos de concesión), arrendamiento (`sys_job_queue`'s lease), Tema / Severidad / Información · Advertencia · Crítica (the messaging bundle), Puesto (the security bundle), principal for Parent (the bundle's word throughout: Unidad de negocio principal, Objeto principal), Mis organizaciones (`es-ES.ts` `nav_account_memberships`), Asignación for Placement (objectui's word). Decommission is retirada; tombstone is lápida. ## Verification Measured at head `d616c8cc3`, a true merge of `origin/main` `fb386074f` (parents `8f3e7cae3`, `fb386074f`). The CLI closure was rebuilt after the merge. - **H2, `pnpm check:i18n`:** - exit 0 with both bundles restored to their `397572ed5` bytes; - exit 1 after the leaf edits alone, `platform-objects DRIFTED (2)`: the two provenance companions (zh-CN's run read `DRIFTED (1)`, one locale); - exit 0 after `pnpm i18n:extract`, which changed only the two companions (−340 / −338 rows, 0 added). Remaining rows vs remaining copies: 43 = 43 and 54 = 54, set difference 0. The mechanism carried over unchanged; the only per-locale difference is which leaves each locale keeps. - exit 0 again at `d616c8cc3` (derived gate run below). - **`pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2`:** 58 files, 942 tests passed (lock verdict command-exit 0), at `d616c8cc3` and before the merge. - **`pnpm --filter @objectstack/platform-objects typecheck`:** exit 0. `tsc -p tsconfig.test.json --listFilesOnly` lists both new ledgers (2 hits). - **Reverse verification.** Five mutation legs through `scripts/ablation-replace.mjs` (anchor hit counts declared, blob change proved, restore proved blob == HEAD and `git diff HEAD` empty each time), each red as predicted: 1. ja-JP both `delegated_admin: "委任管理者"` → `"Delegated Admin"`: 2 failed / 9 passed (verdict row; role test "expected 'Delegated Admin' to match" the Japanese-script pattern). 2. es-ES both `delegated_admin: "Administrador delegado"` → `"Delegated Admin"`: 2 failed / 8 passed (verdict row; "es-ES role delegated_admin reads its en source"). 3. ja-JP declared echo `google: "Google"` → `"グーグル"`: 1 failed / 10 passed ("is a declared echo and must stay the en source"). 4. es-ES declared echo `global: "Global"` (×2) → `"Mundial"`: 1 failed / 9 passed. 5. ja-JP `委任管理者` (×2) → `委託管理者`, still Japanese: 1 failed / 10 passed, only the console-word pin ("expected '委託管理者' to be '委任管理者'"). 6. Sibling leg: `object-lifecycle-panel-echo-decisions.test.ts` restored to its `397572ed5` bytes against the new bundles: 1 failed / 34 passed ("ja-JP sys_oauth_resource.fields.access_token_ttl.label: expected 'アクセストークン TTL' to be 'Access Token TTL'"); restored to HEAD, blob equal. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `d616c8cc3` printed 61 commands. All 61 were run, each exit code written to a file before any pipe. - 60 exited 0 on their first run. - `pnpm check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET (exit 3: `dist/` missing for packages outside the CLI closure). Not a measurement. Rerun after a workspace build (71 of 71 tasks cached): exit 0. - `--ran` reconciliation: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). - **`pnpm check:i18n-coverage`:** exit 0, "621 baselined untranslated string(s), none new". The ratchet is unchanged, as expected. - **ESLint, narrowed and declared:** `eslint --no-inline-config --format json` over the 7 changed `.ts` files: 7 files linted (each confirmed not ignored by the repo config's own `isPathIgnored`), 0 errors, 0 warnings. `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, stated in its own comment), so this diff cannot move a verdict on any untouched file. The repo-wide `pnpm lint` is CI's. - **H6, size:** the derivation reads 2787 changed lines (+1416 / −1371) over 8 files, under the 5000 human-merge threshold. - **NOT MEASURED locally, declared to CI:** the 6 workflow-valued families, the 11 wide-population families and the path-scheduled CI jobs the derivation names. The Invite dialog was not re-driven in a browser (NOT MEASURED: browser rendering; reason: bundle leaves are read directly, as the card measured them). ## Acceptance notes - es-ES renders `Parent` as principal throughout the bundle, so the pre-existing `sys_business_unit.fields.parent_business_unit_id.label` and `sys_user.fields.primary_business_unit_id.label` both read Unidad de negocio principal. This PR follows the bundle's word for `Parent Organization` (Organización principal) and leaves the pre-existing pair alone. Carrier: none. - The ja-JP bundle renders `Locked Until` two ways already (ロック解除日時 on `sys_user`, ロック期限 on `sys_job_queue`). This PR uses ロック解除日時 for `sys_two_factor`, the same lockout concept as `sys_user`. - ja-JP `Managed By` is 管理者, `sys_metadata`'s word, so `sys_email_template` reads 管理者 as the label and as the `admin` option. 管理元 would disambiguate; one line changes it if the maintainer prefers it. - A few authored (not copied) ja-JP / es-ES leaves no longer match their moved `en` source, for example `sys_two_factor.fields.backup_codes.help` (still "JSON-serialized backup codes" where `en` now says an opaque ciphertext) and `sys_notification.description`. Authored leaves are legacy-trusted by the provenance design and outside this card's copy class. Carrier: none. - The eight sibling ledgers' comments name only zh-CN ("since objectstack-ai#20462 zh-CN's objects slice holds only its declared English-by-design leaves"); the `> 0` floors they guard hold for all three locales now. Comment-only drift, not edited here. --- _Generated by [Claude Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20462
Clause-②: no
What changed
A zh-CN console showed English on Setup surfaces: the Invite user dialog listed the membership roles as 所有者 / 管理员 / Delegated Admin / 成员, and a team record read MEMBER COUNT. The keys were present in
packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts, but their values were byte copies of theensource, asos i18n extract --fill=defaultseeds them.sys_memberandsys_invitation) and 成员数.objects-zh-cn-echo-decisions.test.ts. The list is below.zh-CN.source-hashes.generated.tswas regenerated bypnpm i18n:extract, never by hand. It records a leaf only while the leaf is a byte copy of its source, so 320 rows dropped and 42 remain. The 42 are set-equal to the declared echoes.sys_member.fields.role.options.delegated_admin,sys_invitation.fields.role.options.delegated_adminandsys_team.fields.member_count.labelare pinned as translated under zh-CN. The role options must read one word per role on both objects.ja-JPandes-ESare not touched. They carry the same copy class; their counts are under Out of scope.The coverage-ratchet half is out of scope, per triage (5873680175). Teaching
i18n-coverage-baselineto count a copied source string as untranslated widens an existing gate. If the maintainer wants that ratchet, it is a separate gate card. The new ledger deliberately does not assert that no other zh-CN leaf is a copy, for the same reason.Copy count, before and after, by key class
A zh-CN string leaf counts as a copy when it is byte-equal to the
enleaf at the same path, over every leaf ofen.objects.generated.ts/zh-CN.objects.generated.ts. Both bundles have 1529 leaves.label(object, field, view, action param)options.*pluralLabelhelpdescriptionplaceholderemptyState.title/.messageclient.client_id,client.client_secret)label+options.*The card reads 338 in total, 192 of them
label/options.*, at 3cf6449. The zh-CN and en object bundles are byte-identical between 3cf6449 and b810ddb, and this walk reads 362 / 216 at 3cf6449 too. So the difference of 24 comes from the counting method, and the card does not state its method.help/description/placeholder/emptyStateare decided as the same class aslabel. The extractor files each of them as anExpectedEntryunder the samesourcekind as its element's label (field,object,view,action;packages/cli/src/utils/i18n-extract.ts), and the coverage detector consumes them identically. So they were decided as the same class:help,descriptionandemptyStatewere translated, and all four placeholders are declared echoes, because each shows a value the admin types (see the list below).Kept in English by design (42), with reasons
sys_account._actions.link_social.params.provider.options.{google, github, microsoft, apple, facebook, gitlab, discord}(7)fields.id.labelonsys_oauth_application,sys_oauth_access_token,sys_oauth_refresh_token,sys_oauth_consent,sys_oauth_resource,sys_oauth_client_resource,sys_oauth_client_assertion,sys_sso_provider, the eightsys_scim_*objects,sys_email_template,sys_metadata,sys_metadata_history,sys_view_definition,sys_metadata_audit,sys_secret,sys_setting_audit(23)IDhas no noun to render, and adding one would invent content.sys_oauth_application.fields.jwks.label,.jwks_uri.label(2)sys_jwksis authored 签名密钥 (JWKS), and URIs stay URI (重定向 URI). Theirhelpleaves are translated.sys_oauth_application._actions.create_oauth_application.resultDialog.fields.client.client_id/.client_secret(2)sys_sso_provider._actions.register_saml_provider.params.entryPoint.label(1)sys_sso_provider._actions.register_sso_provider.params.{scopes, mapEmail, mapName}.placeholder(3)sys_sso_provider._actions.register_saml_provider.params.identifierFormat.placeholder(1)sys_email.fields.message_id.label(1)sys_setting_audit.fields.source.options.{ui, api}(2)Where the declaration lives. Neither the extract config nor
os i18n extract/os i18n checkhas a keep-source mechanism: no allowlist, no flag, no provenance mark.--filteronly narrows emission. The existing mechanism that declares a leaf "English by design" is this package's echo-decision ledger. The shape istype Verdict = 'translate' | 'echo', with a reason on every row and a departure reason required for everyecho(undeclaredEchoes). It lives inpackages/platform-objects/src/apps/translations/*-echo-decisions.test.ts, one file per family.report-dataset-panel-echo-decisions.test.tsis the first: it hasVerdict, a reason per row and the per-locale departure check written inline. The namedundeclaredEchoespredicate arrives in the later rounds, first inobject-field-editor-panel-echo-decisions.test.ts.objects-zh-cn-echo-decisions.test.tsis that shape applied to the objects bundle. No new mechanism was added.Terminology
The terms follow the zh-CN leaves that were already authored, so one term is not translated two ways:
delegated_adminis 受托管理员. That is the console's own word for this role value (objectuiorganization.roles.delegatedAdmin). The other three roles already read the same in both places (所有者 / 管理员 / 成员). The alternative is 委派管理员, the in-repo word for the ADR-0090 concept (委派管理范围). One line changes it if the maintainer prefers it.sys_scim_group_member.display_title(预配的用户及其所在组). Decommission is 下线.managed_byoptions takesys_metadata's words: 管理方, 平台 / 包 / 管理员.claim, and help text keepsscopes, both as the bundle already writes them (映射:邮箱 claim; 客户端可申请的 scopes …).Webis Web 应用, like the siblingsys_oauth_application.fields.type.options.web.sys_migrationkeeps its section's ASCII punctuation.Collateral: nine existing ledgers encoded the old copy debt
Nine panel ledgers in the same directory asserted floors on the zh-CN provenance table: more than 100 rows, more than 300, and more than 50 echoing objects leaves "to sample". Those floors held only while about 360 zh-CN objects copies existed, so any card that translated them would go red. Each floor's own message states its intent, "provenance table is empty" or "has no echoing objects leaf to sample". Each now asks for greater than 0, with a one-line comment saying why.
One more test pinned
sys_oauth_resource.fields.access_token_ttl.labelas an unauthored fill in all three locales. Its own message says "if a translator authored it, this ledger note is stale". It now records that zh-CN is authored (访问令牌 TTL keeps theTTLtoken, the treatment the lifecycle rows give it), while ja-JP and es-ES stay fills.Files:
action-body-panel-,bare-type-display-,dataset-panel-,field-panel-,hook-execution-panel-,object-collapsed-sections-,object-lifecycle-panel-,page-interface-panel-andreport-form-echo-decisions.test.ts. PR #20485 edits three of these files (field-panel-,object-collapsed-sections-,object-lifecycle-panel-) in different hunks. A local merge-tree run was not taken, so whether the two merge cleanly is not measured.Verification
Measured at head
08ebcd01d. That is a true merge oforigin/main851af0c, and the CLI closure was rebuilt after the merge.pnpm check:i18n(H3):platform-objects DRIFTED (1). The drifted file was the provenance companion;pnpm i18n:extract. The extract changed onlyzh-CN.source-hashes.generated.ts: −320 rows, 0 added, and the bundle file stayed byte-identical.pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2: 56 files and 921 tests passed. Before the collateral edit, the same run had 15 failures across 9 files, all of them the floors above.pnpm --filter @objectstack/platform-objects typecheck: exit 0.tsconfig.test.jsonincludes the new ledger (--listFilesOnly: 1 hit).scripts/ablation-replace.mjsonzh-CN.objects.generated.ts, each red as predicted, and each restored to blob24cfb9355b89withgit diff HEADempty:label: "成员数"changed back to"Member Count": 1 failed / 9 passed, "sys_team.fields.member_count.label reads its en source again".delegated_admin: "受托管理员"changed back to"Delegated Admin": 2 failed / 8 passed. The verdict row failed, and so did the role-word test ("expected 'Delegated Admin' to match /\p{Script=Han}/u").google: "Google"changed to"谷歌": 1 failed / 9 passed, "is a declared echo and must stay the en source".node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat08ebcd01dprinted 61 commands. All 61 were run, and each exit code was written to a file before any pipe.pnpm check:dual-build-cjs-loadsfirst answered PREREQUISITE NOT MET (exit 3: workspacedist/missing for packages outside the CLI closure). That is not a measurement. Rerun once thedist/existed: exit 0.--ranreconciliation: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN (exit 0).pnpm check:i18n-coverage: exit 0, "621 baselined untranslated string(s), none new". The ratchet is unchanged, as expected.Patch round 1
The contract review 5876239898 (PASS at
08ebcd01d) named one false word and two comment slips. All three are corrected here, as text only; no translated value, assertion or floor changed.objects-zh-cn-echo-decisions.test.ts, comment only): it no longer says the provenance table "kept exactly the 42" as an assertion. It now names what is asserted: every declared echo still has its provenance row, and no pinned translation has one. That the table holds nothing beyond the echoes is measured, and deliberately not asserted.undeclaredEchoesto the later rounds rather than toreport-dataset-panel-.origin/mainfc0db22 was merged with a true merge commit. At head927957df4,pnpm check:i18nexited 0 andpnpm --filter @objectstack/platform-objects exec vitest run src/apps/translations/objects-zh-cn-echo-decisions.test.tspassed 10 of 10 (lock verdict: command-exit 0). The round-0 gate readings above were taken at08ebcd01dand were not re-run.Out of scope
ja-JPandes-EScarry the same copy class, measured at b810ddb and not edited:ja-JP: 383 copies (label210 ·options.*20 ·pluralLabel12 ·help121 ·description12 ·placeholder4 ·emptyState4);label+options.*= 230.es-ES: 392 copies (label217 ·options.*22 ·pluralLabel12 ·help121 ·description12 ·placeholder4 ·emptyState4);label+options.*= 239.Acceptance notes
Locked Untiltwo ways, 锁定至 onsys_userand 锁定到 onsys_job_queue. This card uses 锁定至 forsys_two_factorand leaves the pre-existing pair alone.Generated by Claude Code